/*
 * UAE - The Un*x Amiga Emulator
 *
 * scsi.device emulation
 *
 * Copyright 1995 Bernd Schmidt
 * Copyright 1999 Patrick Ohly
 * Copyright 2001 Brian King
 * Copyright 2002 Toni Wilen
 *
 */

#include "sysconfig.h"
#include "execlib.h"
#include "execio.h"
#include "newcpu.h"
#include "native2amiga.h"
#include "blkdev.h"
#include "bsdsocket.h"
#include "custom.h"
#include "win32.h"

// #define CDDEV_COMMANDS

#define UAEDEV_SCSI L"uaescsi.device"
#define UAEDEV_SCSI_ID 1
#define UAEDEV_DISK L"uaedisk.device"
#define UAEDEV_DISK_ID 2

#define MAX_ASYNC_REQUESTS 20
#define MAX_OPEN_DEVICES 20

#define ASYNC_REQUEST_NONE 0
#define ASYNC_REQUEST_TEMP 1
#define ASYNC_REQUEST_CHANGEINT 10
#define ASYNC_REQUEST_FRAMEINT 11

struct devstruct_scsi
{
    int unitnum, aunit;
    int opencnt;
    int changenum;
    int drivetype;
    int iscd;
    volatile uae_ptr d_request[MAX_ASYNC_REQUESTS];
    volatile int d_request_type[MAX_ASYNC_REQUESTS];
    volatile uint d_request_data[MAX_ASYNC_REQUESTS];
    struct device_info di;
    uae_ptr changeint;
    int changeint_mediastate;

    int configblocksize;
    int volumelevel;
    int fadeframes;
    int fadetarget;
    int fadecounter;

    smp_comm_pipe requests;
    int thread_running;
    uae_sem_t sync_sem;
};

struct priv_devstruct
{
    int inuse;
    int unit;
    int mode;
    int type;
    int flags; /* OpenDevice() */
};

static struct devstruct_scsi devst[MAX_TOTAL_SCSI_DEVICES];
static struct priv_devstruct pdevst[MAX_OPEN_DEVICES];
static uint nscmd_cmd;
static uae_sem_t change_sem;

static struct device_info* devinfo(struct devstruct_scsi* devst, struct device_info* di)
{
    struct device_info* dio = sys_command_info(devst->unitnum, di, 0);
    if (dio)
    {
        if (!devst->configblocksize)
            devst->configblocksize = dio->bytespersector;
    }
    return di;
}

static void io_log(const TCHAR* msg, uae_ptr request)
{
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"%s: %08X %d %08X %d %d io_actual=%d io_error=%d\n",
            msg, request, get_word(request + 28), get_long(request + 40),
            get_long(request + 36), get_long(request + 44),
            get_long(request + 32), get_byte(request + 31));
}

static struct devstruct_scsi* getdevstruct(int unit)
{
    int i;
    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        if (unit >= 0 && devst[i].aunit == unit)
            return &devst[i];
    }
    return 0;
}

static struct priv_devstruct* getpdevstruct(uae_ptr request)
{
    int i = get_long(request + 24);
    if (i < 0 || i >= MAX_OPEN_DEVICES || pdevst[i].inuse == 0)
    {
        Logger::Write(L"uaescsi.device: corrupt iorequest %08X %d\n", request, i);
        return 0;
    }
    return &pdevst[i];
}

static TCHAR* getdevname(int type)
{
    switch (type)
    {
        case UAEDEV_SCSI_ID:
            return UAEDEV_SCSI;
        case UAEDEV_DISK_ID:
            return UAEDEV_DISK;
        default:
            return L"nullptr";
    }
}

static void* dev_thread(void* devs);
static int start_thread(struct devstruct_scsi* dev)
{
    if (dev->thread_running)
        return 1;
    init_comm_pipe(&dev->requests, 100, 1);
    uae_sem_init(&dev->sync_sem, 0, 0);
    uae_start_thread(L"uaescsi", dev_thread, dev, nullptr);
    uae_sem_wait(&dev->sync_sem);
    return dev->thread_running;
}

static void dev_close_3(struct devstruct_scsi* dev, struct priv_devstruct* pdev)
{
    if (!dev->opencnt)
        return;
    dev->opencnt--;
    if (!dev->opencnt)
    {
        sys_command_close(dev->unitnum);
        pdev->inuse = 0;
        write_comm_pipe_u32(&dev->requests, 0, 1);
    }
}

static uint REGPARAM2 dev_close_2(TrapContext* context)
{
    uint request = m68k_areg(regs, 1);
    struct priv_devstruct* pdev = getpdevstruct(request);
    struct devstruct_scsi* dev;

    if (!pdev)
        return 0;
    dev = getdevstruct(pdev->unit);
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"%s:%d close, req=%08X\n", getdevname(pdev->type), pdev->unit, request);
    if (!dev)
        return 0;
    dev_close_3(dev, pdev);
    put_long(request + 24, 0);
    put_word(m68k_areg(regs, 6) + 32, get_word(m68k_areg(regs, 6) + 32) - 1);
    return 0;
}

static uint REGPARAM2 dev_close(TrapContext* context)
{
    return dev_close_2(context);
}
static uint REGPARAM2 diskdev_close(TrapContext* context)
{
    return dev_close_2(context);
}

static int openfail(uae_ptr ioreq, int error)
{
    put_long(ioreq + 20, -1);
    put_byte(ioreq + 31, error);
    return (uint) - 1;
}

static uint REGPARAM2 dev_open_2(TrapContext* context, int type)
{
    uae_ptr ioreq = m68k_areg(regs, 1);
    uint unit = m68k_dreg(regs, 0);
    uint flags = m68k_dreg(regs, 1);
    struct devstruct_scsi* dev = getdevstruct(unit);
    struct priv_devstruct* pdev = 0;
    int i;

    if (Logging::SCSI_DEBUG)
        Logger::Write(L"opening %s:%d ioreq=%08X\n", getdevname(type), unit, ioreq);
    if (get_word(ioreq + 0x12) < IOSTDREQ_SIZE && get_word(ioreq + 0x12) > 0)
        return openfail(ioreq, IOERR_BADLENGTH);
    if (!dev)
        return openfail(ioreq, 32);  /* badunitnum */
    if (!dev->opencnt)
    {
        for (i = 0; i < MAX_OPEN_DEVICES; i++)
        {
            pdev = &pdevst[i];
            if (pdev->inuse == 0)
                break;
        }
        if (!sys_command_open(dev->unitnum))
            return openfail(ioreq, IOERR_OPENFAIL);
        pdev->type = type;
        pdev->unit = unit;
        pdev->flags = flags;
        pdev->inuse = 1;
        put_long(ioreq + 24, pdev - pdevst);
        start_thread(dev);
    }
    else
    {
        for (i = 0; i < MAX_OPEN_DEVICES; i++)
        {
            pdev = &pdevst[i];
            if (pdev->inuse && pdev->unit == unit)
                break;
        }
        if (i == MAX_OPEN_DEVICES)
            return openfail(ioreq, IOERR_OPENFAIL);
        put_long(ioreq + 24, pdev - pdevst);
    }
    dev->opencnt++;

    put_word(m68k_areg(regs, 6) + 32, get_word(m68k_areg(regs, 6) + 32) + 1);
    put_byte(ioreq + 31, 0);
    put_byte(ioreq + 8, 7);
    return 0;
}

static uint REGPARAM2 dev_open(TrapContext* context)
{
    return dev_open_2(context, UAEDEV_SCSI_ID);
}
static uint REGPARAM2 diskdev_open(TrapContext* context)
{
    return dev_open_2(context, UAEDEV_DISK_ID);
}

static uint REGPARAM2 dev_expunge(TrapContext* context)
{
    return 0;
}
static uint REGPARAM2 diskdev_expunge(TrapContext* context)
{
    return 0;
}

static int is_async_request(struct devstruct_scsi* dev, uae_ptr request)
{
    int i = 0;
    while (i < MAX_ASYNC_REQUESTS)
    {
        if (dev->d_request[i] == request)
            return 1;
        i++;
    }
    return 0;
}

#if 0
static int scsiemul_switchscsi(const TCHAR* name)
{
    struct devstruct_scsi* dev = nullptr;
    struct device_info* discsi, discsi2;
    int i, opened[MAX_TOTAL_SCSI_DEVICES];
    bool wasopen = false;

    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
        opened[i] = sys_command_isopen(i);

    dev = &devst[0];
    if (dev->opencnt)
        wasopen = true;
    sys_command_close(dev->unitnum);

    dev = nullptr;
    if (device_func_init(DEVICE_TYPE_ANY))
    {
        if (devst[0].di.media_inserted < 0)
            devst[0].di.media_inserted = 0;
        i = 0;
        while (i < MAX_TOTAL_SCSI_DEVICES && dev == nullptr)
        {
            discsi = 0;
            if (sys_command_open(i))
            {
                discsi = sys_command_info(i, &discsi2, 0);
                if (discsi && discsi->type == INQ_ROMD)
                {
                    if (!_tcsicmp(g_curr_conf.cdimagefile[0], discsi->label))
                    {
                        dev = &devst[0];
                        dev->unitnum = i;
                        dev->drivetype = discsi->type;
                        memcpy(&dev->di, discsi, sizeof(struct device_info));
                        dev->iscd = 1;
                        Logger::Write(L"%s mounted as uaescsi.device:0\n", discsi->label);
                        if (dev->di.media_inserted)
                        {
                            dev->di.media_inserted = 0;
                            scsi_do_disk_change(dev->di.id, 1, nullptr);
                        }
                    }
                }
                if (opened[i] == 0 && !wasopen)
                    sys_command_close(i);
            }
            i++;
        }
        if (dev)
            return dev->unitnum;
    }
    return -1;
}
#endif

//  pollmode is 1 if no change interrupts found -> increase time of media change
int scsi_do_disk_change(int unitnum, int insert, int* pollmode)
{
    int i, j, ret;

    ret = -1;
    if (!change_sem)
        return ret;
    uae_sem_wait(&change_sem);
    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        struct devstruct_scsi* dev = &devst[i];
        if (dev->di.unitnum == unitnum + 1)
        {
            ret = i;
            if ((dev->changeint_mediastate > 0 && insert == 0) || (dev->changeint_mediastate <= 0 && insert))
            {
                dev->changeint_mediastate = insert;
                if (pollmode)
                    *pollmode = 1;
                if (dev->aunit >= 0)
                {
                    // struct priv_devstruct* pdev = &pdevst[dev->aunit];
                    devinfo(dev, &dev->di);
                }
                dev->changenum++;
                j = 0;
                while (j < MAX_ASYNC_REQUESTS)
                {
                    if (dev->d_request_type[j] == ASYNC_REQUEST_CHANGEINT)
                    {
                        uae_Cause(dev->d_request_data[j]);
                        if (pollmode)
                            *pollmode = 0;
                    }
                    j++;
                }
                if (dev->changeint)
                {
                    uae_Cause(dev->changeint);
                    if (pollmode)
                        *pollmode = 0;
                }
            }
        }
    }
    uae_sem_post(&change_sem);
    return ret;
}

static int add_async_request(struct devstruct_scsi* dev, uae_ptr request, int type, uint data)
{
    int i;

    if (Logging::SCSI_DEBUG)
        Logger::Write(L"async request %08x (%d) added\n", request, type);
    i = 0;
    while (i < MAX_ASYNC_REQUESTS)
    {
        if (dev->d_request[i] == request)
        {
            dev->d_request_type[i] = type;
            dev->d_request_data[i] = data;
            return 0;
        }
        i++;
    }
    i = 0;
    while (i < MAX_ASYNC_REQUESTS)
    {
        if (dev->d_request[i] == 0)
        {
            dev->d_request[i] = request;
            dev->d_request_type[i] = type;
            dev->d_request_data[i] = data;
            return 0;
        }
        i++;
    }
    return -1;
}

static int release_async_request(struct devstruct_scsi* dev, uae_ptr request)
{
    int i = 0;

    if (Logging::SCSI_DEBUG)
        Logger::Write(L"async request %p removed\n", request);
    while (i < MAX_ASYNC_REQUESTS)
    {
        if (dev->d_request[i] == request)
        {
            int type = dev->d_request_type[i];
            dev->d_request[i] = 0;
            dev->d_request_data[i] = 0;
            dev->d_request_type[i] = 0;
            return type;
        }
        i++;
    }
    return -1;
}

static void abort_async(struct devstruct_scsi* dev, uae_ptr request, int errcode, int type)
{
    int i;
    i = 0;
    while (i < MAX_ASYNC_REQUESTS)
    {
        if (dev->d_request[i] == request && dev->d_request_type[i] == ASYNC_REQUEST_TEMP)
        {
            /* ASYNC_REQUEST_TEMP = request is processing */
            sleep_millis(10);
            i = 0;
            continue;
        }
        i++;
    }
    i = release_async_request(dev, request);
    if (i >= 0 && Logging::SCSI_DEBUG)
        Logger::Write(L"asyncronous request=%08X aborted, error=%d\n", request, errcode);
}

static int command_read(struct devstruct_scsi* dev, uae_ptr data, ulong64 offset, uint length, uint* io_actual)
{
    int blocksize = dev->di.bytespersector;

    length /= blocksize;
    offset /= blocksize;
    while (length > 0)
    {
        byte buffer[4096];
        if (!sys_command_read(dev->unitnum, buffer, offset, 1))
            return 20;
        memcpyha_safe(data, buffer, blocksize);
        data += blocksize;
        offset++;
        length--;
    }
    return 0;
}

static int command_write(struct devstruct_scsi* dev, uae_ptr data, ulong64 offset, uint length, uint* io_actual)
{
    uint blocksize = dev->di.bytespersector;
    length /= blocksize;
    offset /= blocksize;
    while (length > 0)
    {
        byte buffer[4096];
        int err;
        memcpyah_safe(buffer, data, blocksize);
        err = sys_command_write(dev->unitnum, buffer, offset, 1);
        if (!err)
            return 20;
        if (err < 0)
            return 28;  // write protected
        data += blocksize;
        offset++;
        length--;
    }
    return 0;
}

static int command_cd_read(struct devstruct_scsi* dev, uae_ptr data, ulong64 offset, uint length, uint* io_actual)
{
    uint len, sector, startoffset;
    int blocksize;

    blocksize = dev->configblocksize;
    *io_actual = 0;
    startoffset = offset % blocksize;
    offset -= startoffset;
    sector = offset / blocksize;
    while (length > 0)
    {
        byte temp[4096];
        if (blocksize != 2048)
        {
            if (!sys_command_cd_rawread(dev->unitnum, temp, sector, 1, blocksize))
                return 20;
        }
        else
        {
            if (!sys_command_cd_read(dev->unitnum, temp, sector, 1))
                return 20;
        }
        if (startoffset > 0)
        {
            len = blocksize - startoffset;
            if (len > length)
                len = length;
            memcpyha_safe(data, temp + startoffset, len);
            length -= len;
            data += len;
            startoffset = 0;
            *io_actual += len;
        }
        else if (length >= blocksize)
        {
            len = blocksize;
            memcpyha_safe(data, temp, len);
            length -= len;
            data += len;
            *io_actual += len;
        }
        else
        {
            memcpyha_safe(data, temp, length);
            *io_actual += length;
            length = 0;
        }
        sector++;
    }
    return 0;
}

static int dev_do_io(struct devstruct_scsi* dev, uae_ptr request)
{
    uint command;
    uint io_data = get_long(request + 40);  // 0x28
    uint io_length = get_long(request + 36);  // 0x24
    uint io_actual = get_long(request + 32);  // 0x20
    uint io_offset = get_long(request + 44);  // 0x2c
    uint io_error = 0;
    ulong64 io_offset64;
    int async = 0;
    int bmask = dev->di.bytespersector - 1;
    struct priv_devstruct* pdev = getpdevstruct(request);

    if (!pdev)
        return 0;
    command = get_word(request + 28);

    //  Logger::Write(L"%d: DATA=%08X LEN=%08X OFFSET=%08X ACTUAL=%08X\n",
    //          command, io_data, io_length, io_offset, io_actual);

    switch (command)
    {
        case CMD_READ:
            // Logger::Write (L"CMD_READ %08x %d %d %08x\n", io_data, io_offset, io_length, bmask);
            if (dev->di.media_inserted <= 0)
                goto no_media;
            if (dev->drivetype == INQ_ROMD)
            {
                io_error = command_cd_read(dev, io_data, io_offset, io_length, &io_actual);
            }
            else
            {
                if ((io_offset & bmask) || bmask == 0 || io_data == 0)
                    goto bad_command;
                if ((io_length & bmask) || io_length == 0)
                    goto bad_len;
                io_error = command_read(dev, io_data, io_offset, io_length, &io_actual);
            }
            break;
        case TD_READ64:
        case NSCMD_TD_READ64:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            io_offset64 = get_long(request + 44) | ((ulong64)get_long(request + 32) << 32);
            if ((io_offset64 & bmask) || bmask == 0 || io_data == 0)
                goto bad_command;
            if ((io_length & bmask) || io_length == 0)
                goto bad_len;
            if (dev->drivetype == INQ_ROMD)
                io_error = command_cd_read(dev, io_data, io_offset64, io_length, &io_actual);
            else
                io_error = command_read(dev, io_data, io_offset64, io_length, &io_actual);
            break;

        case CMD_WRITE:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            if (dev->di.write_protected || dev->drivetype == INQ_ROMD)
            {
                io_error = 28; /* writeprotect */
            }
            else if ((io_offset & bmask) || bmask == 0 || io_data == 0)
            {
                goto bad_command;
            }
            else if ((io_length & bmask) || io_length == 0)
            {
                goto bad_len;
            }
            else
            {
                io_error = command_write(dev, io_data, io_offset, io_length, &io_actual);
            }
            break;
        case TD_WRITE64:
        case NSCMD_TD_WRITE64:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            io_offset64 = get_long(request + 44) | ((ulong64)get_long(request + 32) << 32);
            if (dev->di.write_protected || dev->drivetype == INQ_ROMD)
            {
                io_error = 28; /* writeprotect */
            }
            else if ((io_offset64 & bmask) || bmask == 0 || io_data == 0)
            {
                goto bad_command;
            }
            else if ((io_length & bmask) || io_length == 0)
            {
                goto bad_len;
            }
            else
            {
                io_error = command_write(dev, io_data, io_offset64, io_length, &io_actual);
            }
            break;

        case CMD_FORMAT:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            if (dev->di.write_protected || dev->drivetype == INQ_ROMD)
            {
                io_error = 28; /* writeprotect */
            }
            else if ((io_offset & bmask) || bmask == 0 || io_data == 0)
            {
                goto bad_command;
            }
            else if ((io_length & bmask) || io_length == 0)
            {
                goto bad_len;
            }
            else
            {
                io_error = command_write(dev, io_data, io_offset, io_length, &io_actual);
            }
            break;
        case TD_FORMAT64:
        case NSCMD_TD_FORMAT64:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            io_offset64 = get_long(request + 44) | ((ulong64)get_long(request + 32) << 32);
            if (dev->di.write_protected || dev->drivetype == INQ_ROMD)
            {
                io_error = 28; /* writeprotect */
            }
            else if ((io_offset64 & bmask) || bmask == 0 || io_data == 0)
            {
                goto bad_command;
            }
            else if ((io_length & bmask) || io_length == 0)
            {
                goto bad_len;
            }
            else
            {
                io_error = command_write(dev, io_data, io_offset64, io_length, &io_actual);
            }
            break;

        case CMD_UPDATE:
        case CMD_CLEAR:
        case CMD_FLUSH:
        case CMD_MOTOR:
        case CMD_SEEK:
            io_actual = 0;
            break;
        case CMD_REMOVE:
            io_actual = dev->changeint;
            dev->changeint = io_data;
            dev->changeint_mediastate = dev->di.media_inserted;
            break;
        case CMD_CHANGENUM:
            io_actual = dev->changenum;
            break;
        case CMD_CHANGESTATE:
            if (dev->di.media_inserted >= 0)
            {
                io_actual = devinfo(dev, &dev->di)->media_inserted > 0 ? 0 : 1;
            }
            else
            {
                io_actual = 1;
            }
            break;
        case CMD_PROTSTATUS:
            io_actual = devinfo(dev, &dev->di)->write_protected ? -1 : 0;
            break;
        case CMD_GETDRIVETYPE:
            io_actual = dev->drivetype;
            break;
        case CMD_GETNUMTRACKS:
            if (dev->di.media_inserted <= 0)
                goto no_media;
            io_actual = dev->di.cylinders;
            break;
        case CMD_GETGEOMETRY:
        {
            struct device_info* di;
            di = devinfo(dev, &dev->di);
            if (di->media_inserted <= 0)
                goto no_media;
            put_long(io_data + 0, di->bytespersector);
            put_long(io_data + 4, di->sectorspertrack * di->trackspercylinder * di->cylinders);
            put_long(io_data + 8, di->cylinders);
            put_long(io_data + 12, di->sectorspertrack * di->trackspercylinder);
            put_long(io_data + 16, di->trackspercylinder);
            put_long(io_data + 20, di->sectorspertrack);
            put_long(io_data + 24, 0);  /* bufmemtype */
            put_byte(io_data + 28, di->type);
            put_byte(io_data + 29, di->removable ? 1 : 0);  /* flags */
            io_actual = 30;
        }
        break;
        case CMD_ADDCHANGEINT:
            dev->changeint_mediastate = dev->di.media_inserted;
            io_error = add_async_request(dev, request, ASYNC_REQUEST_CHANGEINT, io_data);
            if (!io_error)
                async = 1;
            break;
        case CMD_REMCHANGEINT:
            release_async_request(dev, request);
            break;

        case CD_TOCLSN:
        case CD_TOCMSF:
        {
            int msf = command == CD_TOCMSF;
            struct cd_toc_head toc;
            if (sys_command_cd_toc(dev->di.unitnum, &toc))
            {
                if (io_offset == 0 && io_length > 0)
                {
                    int pos = toc.lastaddress;
                    put_byte(io_data, toc.first_track);
                    put_byte(io_data + 1, toc.last_track);
                    if (msf)
                        pos = lsn2msf(pos);
                    put_long(io_data + 2, pos);
                    io_offset++;
                    io_length--;
                    io_data += 6;
                    io_actual++;
                }
                for (int i = toc.first_track_offset; i < toc.last_track_offset && io_length > 0; i++)
                {
                    if (io_offset == toc.toc[i].point)
                    {
                        int pos = toc.toc[i].paddress;
                        put_byte(io_data, (toc.toc[i].control << 4) | toc.toc[i].adr);
                        put_byte(io_data + 1, toc.toc[i].point);
                        if (msf)
                            pos = lsn2msf(pos);
                        put_long(io_data + 2, pos);
                        io_offset++;
                        io_length--;
                        io_data += 6;
                        io_actual++;
                    }
                }
            }
            else
            {
                io_error = IOERR_NotSpecified;
            }
        }
        break;
        case CD_ADDFRAMEINT:
            io_error = add_async_request(dev, request, ASYNC_REQUEST_FRAMEINT, io_data);
            if (!io_error)
                async = 1;
            break;
        case CD_REMFRAMEINT:
            release_async_request(dev, request);
            break;
        case CD_ATTENUATE:
        {
            if (io_offset != -1)
            {
                dev->fadeframes = io_length & 0x7fff;
                dev->fadetarget = io_offset & 0x7fff;
            }
            io_actual = dev->volumelevel;
        }
        break;
        case CD_INFO:
        {
            ushort status = 0;
            struct cd_toc_head toc;
            byte subq[SUBQ_SIZE] = { 0 };
            sys_command_cd_qcode(dev->di.unitnum, subq);
            status |= 1 << 0; // door closed
            if (dev->di.media_inserted)
            {
                status |= 1 << 1;
                status |= 1 << 2; // motor on
                if (sys_command_cd_toc(dev->di.unitnum, &toc))
                {
                    status |= 1 << 3; // toc
                    if (subq[1] == AUDIO_STATUS_IN_PROGRESS || subq[1] == AUDIO_STATUS_PAUSED)
                        status |= 1 << 5;  // audio play
                    if (subq[1] == AUDIO_STATUS_PAUSED)
                        status |= 1 << 6;  // paused
                    if (isdatatrack(&toc, 0))
                        status |= 1 << 4;  // data track
                }
            }
            put_word(io_data + 0, 75);      // PlaySpeed
            put_word(io_data + 2, 1200);    // ReadSpeed (randomly chose 16x)
            put_word(io_data + 4, 1200);    // ReadXLSpeed
            put_word(io_data + 6, dev->configblocksize); // SectorSize
            put_word(io_data + 8, -1);      // XLECC
            put_word(io_data + 10, 0);      // EjectReset
            put_word(io_data + 12, 0);      // Reserved * 4
            put_word(io_data + 14, 0);
            put_word(io_data + 16, 0);
            put_word(io_data + 18, 0);
            put_word(io_data + 20, 1200);   // MaxSpeed
            put_word(io_data + 22, 0xffff); // AudioPrecision (volume)
            put_word(io_data + 24, status); // Status
            put_word(io_data + 26, 0);      // Reserved2 * 4
            put_word(io_data + 28, 0);
            put_word(io_data + 30, 0);
            put_word(io_data + 32, 0);
            io_actual = 34;
        }
        break;
        case CD_CONFIG:
        {
            while (get_long(io_data) != TAG_DONE)
            {
                uint tag = get_long(io_data);
                uint data = get_long(io_data + 4);
                if (tag == 4)
                {
                    //  TAGCD_SECTORSIZE
                    if (data == 2048 || data == 2336 || data == 2352)
                        dev->configblocksize = data;
                    else
                        io_error = IOERR_BADADDRESS;
                }
                io_data += 8;
            }
            break;
        }
        case CD_PAUSE:
        {
            int old = sys_command_cd_pause(dev->di.unitnum, io_length);
            if (old >= 0)
                io_actual = old;
            else
                io_error = IOERR_BADADDRESS;
            break;
        }
        case CD_PLAYLSN:
        {
            int start = io_offset;
            int end = io_length + start;
            if (!sys_command_cd_play(dev->di.unitnum, start, end, 0))
                io_error = IOERR_BADADDRESS;
        }
        break;
        case CD_PLAYMSF:
        {
            int start = msf2lsn(io_offset);
            int end = msf2lsn(io_length) + start;
            if (!sys_command_cd_play(dev->di.unitnum, start, end, 0))
                io_error = IOERR_BADADDRESS;
        }
        break;
        case CD_PLAYTRACK:
        {
            struct cd_toc_head toc;
            int ok = 0;
            if (sys_command_cd_toc(dev->di.unitnum, &toc))
            {
                for (int i = toc.first_track_offset; i < toc.last_track_offset; i++)
                {
                    if (i == io_offset && i + io_length <= toc.last_track_offset)
                    {
                        ok = sys_command_cd_play(dev->di.unitnum, toc.toc[i].address, toc.toc[i + io_length].address, 0);
                        break;
                    }
                }
            }
            if (!ok)
                io_error = IOERR_BADADDRESS;
        }
        break;
        case CD_QCODEMSF:
        case CD_QCODELSN:
        {
            byte subq[SUBQ_SIZE];
            if (sys_command_cd_qcode(dev->di.unitnum, subq))
            {
                if (subq[1] == AUDIO_STATUS_IN_PROGRESS || subq[1] == AUDIO_STATUS_PAUSED)
                {
                    put_byte(io_data + 0, subq[4 + 0]);
                    put_byte(io_data + 1, frombcd(subq[4 + 1]));
                    put_byte(io_data + 2, frombcd(subq[4 + 2]));
                    put_byte(io_data + 3, subq[4 + 6]);
                    int trackpos = fromlongbcd(subq + 4 + 3);
                    int diskpos = fromlongbcd(subq + 4 + 7);
                    if (command == CD_QCODELSN)
                    {
                        trackpos = msf2lsn(trackpos);
                        diskpos = msf2lsn(diskpos);
                    }
                    put_long(io_data + 4, trackpos);
                    put_long(io_data + 8, diskpos);
                    io_actual = 12;
                }
                else
                {
                    io_error = IOERR_InvalidState;
                }
            }
            else
            {
                io_error = IOERR_BADADDRESS;
            }
        }
        break;

        case HD_SCSICMD:
        {
            uint sdd = get_long(request + 40);
            io_error = sys_command_scsi_direct(dev->unitnum, sdd);
            if (Logging::SCSI_DEBUG)
                Logger::Write(L"scsidev: did io: sdd %p request %p error %d\n", sdd, request, get_byte(request + 31));
        }
        break;
        case NSCMD_DEVICEQUERY:
            put_long(io_data + 0, 0);
            put_long(io_data + 4, 16); /* size */
            put_word(io_data + 8, NSDEVTYPE_TRACKDISK);
            put_word(io_data + 10, 0);
            put_long(io_data + 12, nscmd_cmd);
            io_actual = 16;
            break;
        default:
            io_error = IOERR_NOCMD;
            break;
bad_len:
            io_error = IOERR_BADLENGTH;
            break;
bad_command:
            io_error = IOERR_BADADDRESS;
            break;
no_media:
            io_error = TDERR_DiskChanged;
            break;
    }
    put_long(request + 32, io_actual);
    put_byte(request + 31, io_error);
    io_log(L"dev_io", request);
    return async;
}

static int dev_can_quick(uint command)
{
    switch (command)
    {
        case CMD_RESET:
        case CMD_STOP:
        case CMD_START:
        case CMD_CHANGESTATE:
        case CMD_PROTSTATUS:
        case CMD_GETDRIVETYPE:
        case CMD_GETNUMTRACKS:
            return 1;
    }
    return 0;
}

static int dev_canquick(struct devstruct_scsi* dev, uae_ptr request)
{
    uint command = get_word(request + 28);
    return dev_can_quick(command);
}

static uint REGPARAM2 dev_beginio(TrapContext* context)
{
    uint request = m68k_areg(regs, 1);
    byte flags = get_byte(request + 30);
    int command = get_word(request + 28);
    struct priv_devstruct* pdev = getpdevstruct(request);
    struct devstruct_scsi* dev;

    put_byte(request + 8, NT_MESSAGE);
    if (!pdev)
    {
        put_byte(request + 31, 32);
        return get_byte(request + 31);
    }
    dev = getdevstruct(pdev->unit);
    if (!dev)
    {
        put_byte(request + 31, 32);
        return get_byte(request + 31);
    }
    put_byte(request + 31, 0);
    if ((flags & 1) && dev_canquick(dev, request))
    {
        if (dev_do_io(dev, request))
            Logger::Write(L"device %s command %d bug with IO_QUICK\n", getdevname(pdev->type), command);
        return get_byte(request + 31);
    }
    else
    {
        add_async_request(dev, request, ASYNC_REQUEST_TEMP, 0);
        put_byte(request + 30, get_byte(request + 30) & ~1);
        write_comm_pipe_u32(&dev->requests, request, 1);
        return 0;
    }
}

static void* dev_thread(void* devs)
{
    struct devstruct_scsi* dev = (struct devstruct_scsi*)devs;

    uae_set_thread_priority(nullptr, 1);
    dev->thread_running = 1;
    uae_sem_post(&dev->sync_sem);
    for (;;)
    {
        uae_ptr request = (uae_ptr)read_comm_pipe_u32_blocking(&dev->requests);
        uae_sem_wait(&change_sem);
        if (!request)
        {
            dev->thread_running = 0;
            uae_sem_post(&dev->sync_sem);
            uae_sem_post(&change_sem);
            return 0;
        }
        else if (dev_do_io(dev, request) == 0)
        {
            put_byte(request + 30, get_byte(request + 30) & ~1);
            release_async_request(dev, request);
            uae_ReplyMsg(request);
        }
        else
        {
            if (Logging::SCSI_DEBUG)
                Logger::Write(L"%s:%d async request %08X\n", getdevname(0), dev->unitnum, request);
        }
        uae_sem_post(&change_sem);
    }
    return 0;
}

static uint REGPARAM2 dev_init_2(TrapContext* context, int type)
{
    uint base = m68k_dreg(regs, 0);
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"%s init\n", getdevname(type));
    return base;
}

static uint REGPARAM2 dev_init(TrapContext* context)
{
    return dev_init_2(context, UAEDEV_SCSI_ID);
}
static uint REGPARAM2 diskdev_init(TrapContext* context)
{
    return dev_init_2(context, UAEDEV_DISK_ID);
}

static uint REGPARAM2 dev_abortio(TrapContext* context)
{
    uint request = m68k_areg(regs, 1);
    struct priv_devstruct* pdev = getpdevstruct(request);
    struct devstruct_scsi* dev;

    if (!pdev)
    {
        put_byte(request + 31, 32);
        return get_byte(request + 31);
    }
    dev = getdevstruct(pdev->unit);
    if (!dev)
    {
        put_byte(request + 31, 32);
        return get_byte(request + 31);
    }
    put_byte(request + 31, IOERR_ABORTED);
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"abortio %s unit=%d, request=%08X\n", getdevname(pdev->type), pdev->unit, request);
    abort_async(dev, request, IOERR_ABORTED, 0);
    return 0;
}

#define BTL2UNIT(bus, target, lun) (2 * (bus) + (target) / 8) * 100 + (lun) * 10 + (target % 8)

static void dev_reset()
{
    int i, j;
    struct devstruct_scsi* dev;
    int unitnum = 0;

    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        dev = &devst[i];
        if (dev->opencnt > 0)
        {
            for (j = 0; j < MAX_ASYNC_REQUESTS; j++)
            {
                uae_ptr request;
                if (request = dev->d_request[i])
                    abort_async(dev, request, 0, 0);
            }
            dev->opencnt = 1;
            sys_command_close(dev->unitnum);
        }
        memset(dev, 0, sizeof(struct devstruct_scsi));
        dev->unitnum = dev->aunit = -1;
    }
    for (i = 0; i < MAX_OPEN_DEVICES; i++)
        memset(&pdevst[i], 0, sizeof(struct priv_devstruct));

    device_func_init(cdslottype_Default);
    i = 0;
    while (i < MAX_TOTAL_SCSI_DEVICES)
    {
        dev = &devst[i];
        struct device_info* discsi, discsi2;
        if (sys_command_open(i))
        {
            discsi = sys_command_info(i, &discsi2, 0);
            if (discsi)
            {
                dev->unitnum = i;
                dev->drivetype = discsi->type;
                memcpy(&dev->di, discsi, sizeof(struct device_info));
                dev->changeint_mediastate = discsi->media_inserted;
                dev->configblocksize = discsi->bytespersector;
                if (discsi->type == INQ_ROMD)
                    dev->iscd = 1;
            }
        }
        i++;
    }
    unitnum = 0;
    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        dev = &devst[i];
        if (dev->unitnum >= 0)
            sys_command_close(dev->unitnum);
        if (dev->unitnum >= 0 && dev->iscd)
        {
            dev->aunit = unitnum;
            dev->volumelevel = 0x7fff;
            unitnum++;
        }
    }
    if (unitnum == 0)
        unitnum = 1;
    for (i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        dev = &devst[i];
        if (dev->unitnum >= 0)
        {
            if (!dev->iscd)
            {
                dev->aunit = unitnum;
                unitnum++;
            }

            if (Logging::SCSI_EMU_DEBUG)
                Logger::Write(L"%s:%d = %s:'%s'\n", UAEDEV_SCSI, dev->aunit, dev->di.backend, dev->di.label);
        }
        dev->di.label[0] = 0;
    }
}

static uae_ptr ROM_scsidev_resname = 0,
    ROM_scsidev_resid = 0,
    ROM_scsidev_init = 0;

static uae_ptr ROM_diskdev_resname = 0,
    ROM_diskdev_resid = 0,
    ROM_diskdev_init = 0;

static uae_ptr diskdev_startup(uae_ptr resaddr)
{
    /* Build a struct Resident. This will set up and initialize
     * the cd.device */
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"diskdev_startup(0x%x)\n", resaddr);
    put_word(resaddr + 0x0, 0x4AFC);
    put_long(resaddr + 0x2, resaddr);
    put_long(resaddr + 0x6, resaddr + 0x1A);  /* Continue scan here */
    put_word(resaddr + 0xA, 0x8101);  /* RTF_AUTOINIT|RTF_COLDSTART; Version 1 */
    put_word(resaddr + 0xC, 0x0305);  /* NT_DEVICE; pri 05 */
    put_long(resaddr + 0xE, ROM_diskdev_resname);
    put_long(resaddr + 0x12, ROM_diskdev_resid);
    put_long(resaddr + 0x16, ROM_diskdev_init);
    resaddr += 0x1A;
    return resaddr;
}

uae_ptr scsidev_startup(uae_ptr resaddr)
{
    if (g_curr_conf.scsi != 1)
        return resaddr;
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"scsidev_startup(0x%x)\n", resaddr);
    /* Build a struct Resident. This will set up and initialize
     * the uaescsi.device */
    put_word(resaddr + 0x0, 0x4AFC);
    put_long(resaddr + 0x2, resaddr);
    put_long(resaddr + 0x6, resaddr + 0x1A);  /* Continue scan here */
    put_word(resaddr + 0xA, 0x8101);  /* RTF_AUTOINIT|RTF_COLDSTART; Version 1 */
    put_word(resaddr + 0xC, 0x0305);  /* NT_DEVICE; pri 05 */
    put_long(resaddr + 0xE, ROM_scsidev_resname);
    put_long(resaddr + 0x12, ROM_scsidev_resid);
    put_long(resaddr + 0x16, ROM_scsidev_init);  /* calls scsidev_init */
    resaddr += 0x1A;
    return resaddr;
    // return diskdev_startup(resaddr);
}

static void diskdev_install()
{
    uint functable, datatable;
    uint initcode, openfunc, closefunc, expungefunc;
    uint beginiofunc, abortiofunc;

    if (g_curr_conf.scsi != 1)
        return;
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"diskdev_install(): 0x%x\n", here());

    ROM_diskdev_resname = ds(UAEDEV_DISK);
    ROM_diskdev_resid = ds(L"UAE disk.device 0.1");

    /* initcode */
    initcode = here();
    calltrap(deftrap(diskdev_init));
    dw(RTS);

    /* Open */
    openfunc = here();
    calltrap(deftrap(diskdev_open));
    dw(RTS);

    /* Close */
    closefunc = here();
    calltrap(deftrap(diskdev_close));
    dw(RTS);

    /* Expunge */
    expungefunc = here();
    calltrap(deftrap(diskdev_expunge));
    dw(RTS);

    /* BeginIO */
    beginiofunc = here();
    calltrap(deftrap(dev_beginio));
    dw(RTS);

    /* AbortIO */
    abortiofunc = here();
    calltrap(deftrap(dev_abortio));
    dw(RTS);

    /* FuncTable */
    functable = here();
    dl(openfunc);  /* Open */
    dl(closefunc);  /* Close */
    dl(expungefunc);  /* Expunge */
    dl(EXPANSION_nullfunc);  /* Null */
    dl(beginiofunc);  /* BeginIO */
    dl(abortiofunc);  /* AbortIO */
    dl(0xFFFFFFFFul);  /* end of table */

    /* DataTable */
    datatable = here();
    dw(0xE000);  /* INITBYTE */
    dw(0x0008);  /* LN_TYPE */
    dw(0x0300);  /* NT_DEVICE */
    dw(0xC000);  /* INITLONG */
    dw(0x000A);  /* LN_NAME */
    dl(ROM_diskdev_resname);
    dw(0xE000);  /* INITBYTE */
    dw(0x000E);  /* LIB_FLAGS */
    dw(0x0600);  /* LIBF_SUMUSED | LIBF_CHANGED */
    dw(0xD000);  /* INITWORD */
    dw(0x0014);  /* LIB_VERSION */
    dw(0x0004);  /* 0.4 */
    dw(0xD000);  /* INITWORD */
    dw(0x0016);  /* LIB_REVISION */
    dw(0x0000);  /* end of table already ??? */
    dw(0xC000);  /* INITLONG */
    dw(0x0018);  /* LIB_IDSTRING */
    dl(ROM_diskdev_resid);
    dw(0x0000);  /* end of table */

    ROM_diskdev_init = here();
    dl(0x00000100);  /* size of device base */
    dl(functable);
    dl(datatable);
    dl(initcode);
}

void scsidev_install()
{
    uint functable, datatable;
    uint initcode, openfunc, closefunc, expungefunc;
    uint beginiofunc, abortiofunc;

    if (g_curr_conf.scsi != 1)
        return;
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"scsidev_install(): 0x%x\n", here());

    ROM_scsidev_resname = ds(UAEDEV_SCSI);
    ROM_scsidev_resid = ds(L"UAE scsi.device 0.2");

    /* initcode */
    initcode = here();
    calltrap(deftrap(dev_init));
    dw(RTS);

    /* Open */
    openfunc = here();
    calltrap(deftrap(dev_open));
    dw(RTS);

    /* Close */
    closefunc = here();
    calltrap(deftrap(dev_close));
    dw(RTS);

    /* Expunge */
    expungefunc = here();
    calltrap(deftrap(dev_expunge));
    dw(RTS);

    /* BeginIO */
    beginiofunc = here();
    calltrap(deftrap(dev_beginio));
    dw(RTS);

    /* AbortIO */
    abortiofunc = here();
    calltrap(deftrap(dev_abortio));
    dw(RTS);

    /* FuncTable */
    functable = here();
    dl(openfunc);  /* Open */
    dl(closefunc);  /* Close */
    dl(expungefunc);  /* Expunge */
    dl(EXPANSION_nullfunc);  /* Null */
    dl(beginiofunc);  /* BeginIO */
    dl(abortiofunc);  /* AbortIO */
    dl(0xFFFFFFFFul);  /* end of table */

    /* DataTable */
    datatable = here();
    dw(0xE000);  /* INITBYTE */
    dw(0x0008);  /* LN_TYPE */
    dw(0x0300);  /* NT_DEVICE */
    dw(0xC000);  /* INITLONG */
    dw(0x000A);  /* LN_NAME */
    dl(ROM_scsidev_resname);
    dw(0xE000);  /* INITBYTE */
    dw(0x000E);  /* LIB_FLAGS */
    dw(0x0600);  /* LIBF_SUMUSED | LIBF_CHANGED */
    dw(0xD000);  /* INITWORD */
    dw(0x0014);  /* LIB_VERSION */
    dw(0x0004);  /* 0.4 */
    dw(0xD000);  /* INITWORD */
    dw(0x0016);  /* LIB_REVISION */
    dw(0x0000);
    dw(0xC000);  /* INITLONG */
    dw(0x0018);  /* LIB_IDSTRING */
    dl(ROM_scsidev_resid);
    dw(0x0000);  /* end of table */

    ROM_scsidev_init = here();
    dl(0x00000100);  /* size of device base */
    dl(functable);
    dl(datatable);
    dl(initcode);

    nscmd_cmd = here();
    dw(NSCMD_DEVICEQUERY);
    dw(CMD_RESET);
    dw(CMD_READ);
    dw(CMD_WRITE);
    dw(CMD_UPDATE);
    dw(CMD_CLEAR);
    dw(CMD_START);
    dw(CMD_STOP);
    dw(CMD_FLUSH);
    dw(CMD_MOTOR);
    dw(CMD_SEEK);
    dw(CMD_FORMAT);
    dw(CMD_REMOVE);
    dw(CMD_CHANGENUM);
    dw(CMD_CHANGESTATE);
    dw(CMD_PROTSTATUS);
    dw(CMD_GETDRIVETYPE);
    dw(CMD_GETGEOMETRY);
    dw(CMD_ADDCHANGEINT);
    dw(CMD_REMCHANGEINT);
    dw(HD_SCSICMD);
    dw(NSCMD_TD_READ64);
    dw(NSCMD_TD_WRITE64);
    dw(NSCMD_TD_SEEK64);
    dw(NSCMD_TD_FORMAT64);
    dw(0);

    diskdev_install();
}

void scsidev_start_threads()
{
    if (g_curr_conf.scsi != 1) /* quite useless.. */
        return;
    if (Logging::SCSI_DEBUG)
        Logger::Write(L"scsidev_start_threads()\n");
    uae_sem_init(&change_sem, 0, 1);
}

void scsidev_reset()
{
    if (g_curr_conf.scsi != 1)
        return;
    dev_reset();
}